﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections.Concurrent;
using System.Reflection.Emit;
using PServiceBus.MessageProxy.Interface;
using System.Threading.Tasks;


namespace PServiceBus.MessageProxy {
    internal static class ProxyHelper {

        private static readonly ConcurrentDictionary<Type, Type> CachedType = new ConcurrentDictionary<Type, Type>();
        private static readonly ConcurrentDictionary<string, Type> CachedRequestType = new ConcurrentDictionary<string, Type>();
        private const MethodAttributes ConstructorAttribute = MethodAttributes.Public | MethodAttributes.HideBySig;

        private const BindingFlags PropertyBinding = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;

        private const MethodAttributes MethodAttribute =
            MethodAttributes.Public
            | MethodAttributes.Virtual
            | MethodAttributes.Final
            | MethodAttributes.HideBySig
            | MethodAttributes.NewSlot
            | MethodAttributes.SpecialName;

        private static Type TypeType = typeof(Type);

        private static Type VoidType = typeof(void);

        private static Type ObjectType = typeof(object);

        private static Type ActionType = typeof(Action<>);

        private static Type DictType = typeof(Dictionary<string, object>);

        private static MethodInfo ESBHelperInstance = typeof(ESBHelper).GetProperty("Instance").GetGetMethod();

        private static ConstructorInfo ObjectCtr =
            typeof(Object).GetConstructor(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
            Type.EmptyTypes, null);

        private static ConstructorInfo DictCtor =
            DictType.GetConstructor(Type.EmptyTypes);

        private static void WriteListenerMethod(MethodInfo method, ILGenerator il) {
            var listenAttr = method.GetAttribute<ESBListenerAttribute>();
            var intervalAttr = method.GetAttribute<ESBIntervalAttribute>();

            var parameters = method.GetParameters();
            if (parameters.Length < 2)
                throw new InvalidOperationException("Method using Listener attribute must have two parameters (Action<T>, Func<Exception, bool>)");
            if (parameters.Length > 2)
                throw new InvalidOperationException("Too many argument for listener method. Only (Action<T>, Func<Exception, bool>) is permitted");
            var receivedParameter = parameters[0];
            var exceptionParameter = parameters[1];
            if (receivedParameter.ParameterType.GetGenericTypeDefinition() != typeof(Action<>))
                throw new InvalidOperationException("First parameter must be Action<T>");
            if (exceptionParameter.ParameterType != typeof(Func<Exception, bool>))
                throw new InvalidOperationException("Second parameter must be Action<Exception>");

            var transportMethod = ESBHelper.Select("GetTransport");
            var startListenerMethod = ESBHelper.Select("StartListener")
                .MakeGenericMethod(receivedParameter.ParameterType.GetGenericArguments()[0]);

            il.Emit(OpCodes.Call, ESBHelperInstance);

            il.Emit(OpCodes.Ldc_I4, intervalAttr != null ? intervalAttr.Interval : 0);

            il.Emit(OpCodes.Ldstr, listenAttr.ID);
            il.Emit(OpCodes.Ldstr, listenAttr.Topic);

            il.Emit(OpCodes.Call, ESBHelperInstance);
            il.Emit(OpCodes.Ldtoken, method);
            il.Emit(OpCodes.Call,
                typeof(MethodBase).GetMethod("GetMethodFromHandle",
                new Type[] { typeof(RuntimeMethodHandle) }));
            il.Emit(OpCodes.Callvirt, transportMethod);

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldarg_2);

            il.Emit(OpCodes.Callvirt, startListenerMethod);
        }

        private static void WriteEndListenerMethod(MethodInfo method, ILGenerator il) {
            var endListenAttr = method.GetAttribute<ESBEndListenerAttribute>();
            var endListenerMethod = ESBHelper.Select("EndListener");
            var parameters = method.GetParameters();

            if (parameters.Length > 0)
                throw new InvalidOperationException("End listener method does not accept parameters");

            il.Emit(OpCodes.Call, ESBHelperInstance);

            il.Emit(OpCodes.Ldstr, endListenAttr.ID);
            il.Emit(OpCodes.Ldstr, endListenAttr.Topic);

            il.Emit(OpCodes.Callvirt, endListenerMethod);
        }

        private static void WriteVoidMethod(MethodInfo method, ILGenerator il) {
            var requestAttr = method.GetAttribute<ESBRequestAttribute>();
            var listenAttr = method.GetAttribute<ESBListenerAttribute>();
            var endListenAttr = method.GetAttribute<ESBEndListenerAttribute>();
            var isListen = listenAttr != null;
            var isEndListen = endListenAttr != null;
            var topicName = requestAttr != null ? requestAttr.Topic : method.Name;
            var isSingle = requestAttr != null ? requestAttr.IsRequestClass : false;
            var sendMethod = isSingle ? ESBHelper.Select("SendOneWayRequest") : ESBHelper.Select("SendOneWayDict");
            var translateMethod = ESBHelper.Select("Translate");
            var parameters = method.GetParameters();

            if (isListen) {
                WriteListenerMethod(method, il);
                return;
            }

            if (isEndListen) {
                WriteEndListenerMethod(method, il);
                return;
            }

            if (!isSingle) {

                var dictLocal = il.DeclareLocal(DictType);
                var dictSetItem = DictType.GetProperty("Item").GetSetMethod();

                il.Emit(OpCodes.Newobj, DictCtor);
                il.Emit(OpCodes.Stloc, dictLocal.LocalIndex);

                //Build Parameters
                for (var i = 0; i < parameters.Length; i++) {
                    var param = parameters[i];
                    var paramType = param.ParameterType;
                    var paramAttr = param.GetAttribute<ESBParamAttribute>();
                    var key = paramAttr != null ? paramAttr.Name : param.Name;
                    il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
                    il.Emit(OpCodes.Ldstr, key);
                    il.Emit(OpCodes.Call, ESBHelperInstance);
                    il.Emit(OpCodes.Ldarg, i + 1);
                    if (paramType.IsValueType)
                        il.Emit(OpCodes.Box, paramType);
                    il.Emit(OpCodes.Callvirt, translateMethod);
                    il.Emit(OpCodes.Callvirt, dictSetItem);
                }

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldstr, topicName);
                il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
                il.Emit(OpCodes.Callvirt, sendMethod);
            } else {
                sendMethod = sendMethod.MakeGenericMethod(parameters[0].ParameterType);

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldstr, topicName);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Callvirt, sendMethod);
            }
        }

        private static void WriteTypedMethod(MethodInfo method, ILGenerator il) {
            var requestAttr = method.GetAttribute<ESBRequestAttribute>();
            var timeoutAttr = method.GetAttribute<ESBTimeoutAttribute>();
            var transportMethod = ESBHelper.Select("GetTransport");
            var translateMethod = ESBHelper.Select("Translate");

            var returnType = method.ReturnType;
            var timeout = timeoutAttr != null ? timeoutAttr.Timeout : 0;
            var topicName = requestAttr != null ? requestAttr.Topic : method.Name;
            var parameters = method.GetParameters();
            var isSingle = requestAttr != null ? requestAttr.IsRequestClass : false;
            var sendMethodName = isSingle ? "SendWithRequest" : "SendWithDict";


            if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) {
                sendMethodName += "Task";
                returnType = returnType.GetGenericArguments()[0];
            }

            var sendMethod = ESBHelper.Select(sendMethodName);            
            
            if (!isSingle) {
                sendMethod = sendMethod.MakeGenericMethod(returnType);

                var dictLocal = il.DeclareLocal(DictType);
                var dictSetItem = DictType.GetProperty("Item").GetSetMethod();

                il.Emit(OpCodes.Newobj, DictCtor);
                il.Emit(OpCodes.Stloc, dictLocal.LocalIndex);

                //Build Parameters
                for (var i = 0; i < parameters.Length; i++) {
                    var param = parameters[i];
                    var paramType = param.ParameterType;
                    var paramAttr = param.GetAttribute<ESBParamAttribute>();
                    var key = paramAttr != null ? paramAttr.Name : param.Name;
                    il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
                    il.Emit(OpCodes.Ldstr, key);
                    il.Emit(OpCodes.Call, ESBHelperInstance);
                    il.Emit(OpCodes.Ldarg, i + 1);
                    if (paramType.IsValueType)
                        il.Emit(OpCodes.Box, paramType);
                    il.Emit(OpCodes.Callvirt, translateMethod);
                    il.Emit(OpCodes.Callvirt, dictSetItem);
                }

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldstr, topicName);

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldtoken, method);
                il.Emit(OpCodes.Call,
                    typeof(MethodBase).GetMethod("GetMethodFromHandle",
                    new Type[] { typeof(RuntimeMethodHandle) }));
                il.Emit(OpCodes.Callvirt, transportMethod);

                il.Emit(OpCodes.Ldc_I4, timeout);
                il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
                il.Emit(OpCodes.Callvirt, sendMethod);
            } else {
                sendMethod = sendMethod.MakeGenericMethod(parameters[0].ParameterType, returnType);

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldstr, topicName);

                il.Emit(OpCodes.Call, ESBHelperInstance);
                il.Emit(OpCodes.Ldtoken, method);
                il.Emit(OpCodes.Call,
                    typeof(MethodBase).GetMethod("GetMethodFromHandle",
                    new Type[] { typeof(RuntimeMethodHandle) }));
                il.Emit(OpCodes.Callvirt, transportMethod);

                il.Emit(OpCodes.Ldc_I4, timeout);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Callvirt, sendMethod);
            }
        }

        internal static Type CreateProxy<T>() where T : class {
            var classType = typeof(T);
            if (CachedType.ContainsKey(classType)) return CachedType[classType];
            if (!classType.IsInterface) throw new InvalidOperationException("Type must be an interface");

            var serviceAttributes = classType.GetAttributes<ESBProxyServiceAttribute>();
            if (!serviceAttributes.Any()) throw new InvalidOperationException("Interface must be marked with ESBProxyService attribute");
            var newTypeName = String.Concat(classType.Name);

            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(newTypeName) { Version = new Version(1, 0, 0, 0) },
                                                                           AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(newTypeName + ".dll");
            var type = module.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Sealed, ObjectType, new[] { classType });

            //Define constructor
            var ctor = type.DefineConstructor(MethodAttributes.Public
                | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName |
                MethodAttributes.HideBySig, CallingConventions.Standard, Type.EmptyTypes);

            var ctorIL = ctor.GetILGenerator();
            var setup = ESBHelper.Select("Setup");
            //Call base.object.ctor
            ctorIL.Emit(OpCodes.Ldarg_0);
            ctorIL.Emit(OpCodes.Call, ObjectCtr);
            //write constructor body
            foreach (var attr in serviceAttributes) {
                ctorIL.Emit(OpCodes.Call, ESBHelperInstance);
                ctorIL.Emit(OpCodes.Ldstr, attr.Host);
                ctorIL.Emit(OpCodes.Ldc_I4, attr.Port);
                ctorIL.Emit(OpCodes.Callvirt, setup);
            }
            ctorIL.Emit(OpCodes.Ret);

            //Define methods
            var methods = classType.GetMethods();

            foreach (var method in methods) {
                if (method.Name.StartsWith("set_") || method.Name.StartsWith("get_")) continue;

                var methodName = method.Name;
                var parameters = method.GetParameters();
                var methodReturnType = method.ReturnType;
                var isVoid = methodReturnType == VoidType;
                var isProxy = method.GetCustomAttributes(typeof(ESBIgnoreMethodAttribute), true).Count() == 0;

                var methodBuilder = type.DefineMethod(methodName, MethodAttribute, method.ReturnType,
                    parameters.Select(p => p.ParameterType).ToArray());

                var methodIL = methodBuilder.GetILGenerator();
                
                if (isProxy) {
                    if (isVoid) {
                        WriteVoidMethod(method, methodIL);
                    } else {
                        WriteTypedMethod(method, methodIL);
                    }
                }

                methodIL.Emit(OpCodes.Ret);
                type.DefineMethodOverride(methodBuilder, method);
            }

            var returnType = type.CreateType();
            CachedType[typeof(T)] = returnType;
            //assembly.Save(String.Concat(newTypeName + ".dll"));
            return returnType;
        }

        internal static IServerStarter _serverStarter = null;

        internal static void StartServers() {

            if (_serverStarter != null) {
                _serverStarter.Start();
                return;
            }

            var types = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(x => x.GetTypes()).Where(x => x.GetInterfaces()
                .Any(y => y.Name.Contains(typeof(IESBMessage<,>).Name))).ToList();

            var newTypeName = typeof(IServerStarter).Name;

            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(newTypeName) { Version = new Version(1, 0, 0, 0) },
                                                                           AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(newTypeName + ".dll");
            var typeBuilder = module.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Sealed, ObjectType, new[] { typeof(IServerStarter) });


            var handleMethod = ESBHelper.Select("Handle");
            var transportMethod = ESBHelper.Select("GetTransport");

            var field = typeBuilder.DefineField("_set", typeof(bool), FieldAttributes.Private | FieldAttributes.HasDefault);

            var method = typeBuilder.DefineMethod("Start", MethodAttribute, VoidType, Type.EmptyTypes);

            var il = method.GetILGenerator();
            var isSet = il.DefineLabel();

            //Only setup server once in order to preserve stored states
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, field);
            il.Emit(OpCodes.Brfalse, isSet);
            il.Emit(OpCodes.Ret);
            il.MarkLabel(isSet);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Stfld, field);

            foreach (var type in types) {
                var typeLocal = il.DeclareLocal(type);
                
                il.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));
                il.Emit(OpCodes.Stloc, typeLocal.LocalIndex);

                var methods = type.GetMethods();

                foreach (var typeMethod in methods) {
                    if (!typeMethod.Name.StartsWith("Handle")) continue;
                    var paramTypes = new[] { typeMethod.GetParameters()[0].ParameterType, typeMethod.ReturnType };
                    var intervalAttr = typeMethod.GetAttribute<ESBIntervalAttribute>();

                    il.Emit(OpCodes.Call, ESBHelperInstance);

                    il.Emit(OpCodes.Ldc_I4, intervalAttr != null ? intervalAttr.Interval : 0);

                    il.Emit(OpCodes.Call, ESBHelperInstance);
                    il.Emit(OpCodes.Ldtoken, typeMethod);
                    il.Emit(OpCodes.Call,
                        typeof(MethodBase).GetMethod("GetMethodFromHandle",
                        new Type[] { typeof(RuntimeMethodHandle) }));
                    il.Emit(OpCodes.Callvirt, transportMethod);

                    il.Emit(OpCodes.Ldloc, typeLocal.LocalIndex);
                    il.Emit(OpCodes.Ldftn, typeMethod);
                    il.Emit(OpCodes.Newobj, typeof(Func<,>)
                        .MakeGenericType(paramTypes)
                        .GetConstructor(new[] { typeof(object), typeof(IntPtr) }));

                    il.Emit(OpCodes.Callvirt, handleMethod.MakeGenericMethod(paramTypes));
                }
            }

            il.Emit(OpCodes.Ret);

            typeBuilder.DefineMethodOverride(method, typeof(IServerStarter).GetMethod("Start"));

            var createdType = typeBuilder.CreateType();
            _serverStarter = Activator.CreateInstance(createdType) as IServerStarter;
            //assembly.Save(String.Concat(newTypeName + ".dll"));
            _serverStarter.Start();
        }

        internal static void Shutdown() {
            ESBHelper.Instance.Shutdown();
        }
    }
    public interface IServerStarter {
        void Start();
    }
}
